"""
================================================================================

    abc2esac - ABC to EsAC conversion
    Copyright (C) 2010, Marcus Weseloh

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.


================================================================================
"""

import re
from fractions import Fraction as F

from simpleparse.parser import Parser
from simpleparse.dispatchprocessor import dispatchList, DispatchProcessor 

from abcesac.music import Bar, Note, Rest, Tuplet, Key

ABC_HEADER_RE = re.compile('^([XTABCDEGHILMNOPQRSZKVwW]):\s*(.*)$')
ABC_COMMENT_RE = re.compile('^\s*[#%].*$')
ABC_KEY_RE = re.compile('^([CDEFGAB][b#]?)(.*)$')
ABC_METER_RE = re.compile('([0-9]+)/([0-9]+)')
ABC_VOICE_RE = re.compile('^\s*([a-z0-9=._-]+)(.*)$')

BASE_NOTES = ['C', 'D', 'E', 'F', 'G', 'A', 'B']

ABC_HEADERS = { 
    'X': 'index',
    'T': 'title',
    'B': 'book',
    'C': 'composer',
    'D': 'discography',
    'I': 'information',
    'H': 'history',
    'O': 'origin',
    'S': 'source',
    'Z': 'transscrnotes',
    'N': 'notes',
    'Q': 'tempo',
    'R': 'rhythm',
    'M': 'meter',
    'L': 'base_length',
    'K': 'key',
    'V': 'voice',
    'w': 'line_lyrics',
    'W': 'lyrics'
}

ABC_MODES = {
    'm': 'minor',
    'minor': 'minor',
    'min': 'minor',
    'maj': 'major',
    'major': 'major',
    'lyd': 'lydian',
    'ion': 'ionian',
    'mix': 'mixolydian',
    'dor': 'dorian',
    'aeo': 'aeolian',
    'phr': 'phrygian',
    'loc': 'locrian',
}

class ABCParser(object):
    """Parses ABC and updated the passed in song"""
    def __init__(self, song):
        self.song = song
        self.processor = None

    def parse(self, source):
        counter = 0
        for line in source.split('\n'):
            counter += 1
            line = line.strip()
            if line == '':
                continue
            if ABC_COMMENT_RE.match(line):
                continue
            try:
                match = ABC_HEADER_RE.match(line)
                if match:
                    if match.group(1) not in ABC_HEADERS:
                        raise Exception('Unknown ABC header: %s' % line)
                    self.parse_header(ABC_HEADERS[match.group(1)], match.group(2))
                else:
                    # anything else must be musical notes...
                    self.parse_music(line)
            except Exception, e:
                if self.song and self.song.title:
                    raise Exception('Error processing "%s", input line %d: %s\n%s' % (self.song.title, counter, e, line))
                else:
                    raise Exception('Error processing ABC input line %d: %s\n%s' % (counter, e, line))
        if self.processor:
            self.song.bars = [b for b in self.processor.bars if len(b.elements)]
            # import pprint
            # print '=============='
            # for bar in self.song.bars:
            # pprint.pprint(bar.elements)

    def parse_music(self, line):
        if not self.processor:
            self.processor = ABCProcessor(self.song.meter, self.song.key,
                                          self.song.mode, self.song.base_length)
        success, _, _ = ABCLineParser.parse(line+'\n', 
                                                 processor=self.processor)
        if not success:
            raise Exception('Unable to parse line: %s' % line)
        #self.song.elements.extend(result[0])

    def parse_header(self, header, value):
        method = getattr(self, 'parse_%s' % header, None)
        if method:
            method(value)
        else:
            val = getattr(self.song, header)
            if val:
                val += '\n%s' % value
            else:
                val = value
            setattr(self.song, header, val)

    def parse_voice(self, value):
        pass

    def parse_line_lyrics(self, value):
        pass

    def parse_lyrics(self, value):
        pass

    def parse_index(self, value):
        self.song.index = int(value)

    def parse_key(self, value):
        match = ABC_KEY_RE.match(value)
        if not match:
            raise Exception('Invalid key spec: %s' % value)
        basenote = match.group(1)
        mode = match.group(2).strip().lower()
        if mode == '_':
            basenote += 'b'
            mode = 'major'
        if not mode:
            mode = 'major'
        else:
            mode = ABC_MODES[mode]
        self.song.key = basenote
        self.song.mode = mode

    def parse_meter(self, value):
        if value.upper() == 'C':
            self.song.meter = (4, 4)
        elif value.upper() == 'C|':
            self.song.meter = (2, 2)
        else:
            match = ABC_METER_RE.match(value)
            if not match:
                raise Exception('Invalid meter specification: %s' % value)
            self.song.meter = (int(match.group(1)), int(match.group(2)))

    def parse_base_length(self, value):
        value = value.strip().split(' ')[0]
        self.song.base_length = F(value)


class ABCProcessor(DispatchProcessor):
    def __init__(self, meter, key, mode, base_length):
        self._meter = meter
        self._base_length = base_length
        self._note_diminish = 0
        self._note_augment = 0
        self._note_natural = False
        self._track = None
        self._key = Key(key)
        self._mode = mode
        self._mode_scale = self._key.mode_scale(self._mode)
        self._broken_rhythm = ''
        self._broken_count = 0

        self._pending_element = None
        self._element = None
        self._bar = None
        self.bars = []

        self._tuplet_count = 0
        self._tuplet = None
        self._start_repeat = 0
        self._end_repeat = None

    ###### production methods

    def broken_rhythm(self, value, buf):
        val = self.get_string(value, buf)
        note = self._element
        for i in range(val.count('>')):
            note.dots += 1
            self._broken_rhythm = 'half'
            self._broken_count += 1
        for i in range(val.count('<')):
            note.set_length(note.length / 2)
            self._broken_rhythm = 'dot'
            self._broken_count += 1

    def barline(self, value, buf):
        val = self.get_string(value, buf)
        if val.startswith(':'):
            self._bar.end_repeat = True
        self.start_bar()
        if ':' in val or val.endswith(']'):
            self._bar.start_repeat = True

    # nth_repeat ::=  ':|2' / '[1' / '[2' / '|1'
    def nth_repeat(self, value, buf):
        val = self.get_string(value, buf)
        if val.startswith(':'):
            self._bar.end_repeat = True
        if '|' in val:
            self.start_bar()
        if val.endswith('1'):
            self._bar.nth_repeat = 1
        elif val.endswith('2'):
            self._bar.nth_repeat = 2

    def abc_line(self, value, buf):
        dispatchList(self, value[3], buf)
        return self.bars

    def mid_tune_field(self, value, buf):
        print 'WARNING: ignoring mid-tune field: %s' % self.get_string(value, buf)

    def basenote(self, value, buf):
        val = self.get_string(value, buf)
        octave = 4
        if val.islower():
            octave += 1
            val = val.upper()
        self._element.name = val
        self._element.octave = octave
        for i in range(self._note_augment):
            self._element.augment()
        for i in range(self._note_diminish):
            self._element.diminish()
        note = self._element.name
        # ABC uses implicit accidentals by key, normalize basenote according to scale
        if len(note) == 1 and note not in self._mode_scale:
            offset = BASE_NOTES.index(self._key.key[0])
            note_idx = BASE_NOTES.index(note)
            self._element.name = self._mode_scale[note_idx - offset]
        if self._note_natural:
            if len(self._element.name) > 1:
                self._element.name = self._element.name[0]

    def note(self, value, buf):
        dispatchList(self, value[3], buf)
        if self._broken_rhythm == 'dot':
            self._element.dots += self._broken_count
        elif self._broken_rhythm == 'half':
            for i in range(self._broken_count):
                self._element.length /= 2
        self._broken_rhythm = ''
        self._broken_count  = 0

    def tuplet_element(self, value, buf):
        dispatchList(self, value[3], buf)

    def tuplet_spec(self, value, buf):
        val = self.get_string(value, buf)
        toks = val[1:].split(':')
        if len(toks) == 1:
            tuplet_count = int(toks[0])
            base_count = None
            note_count = tuplet_count
        elif len(toks) == 2:
            tuplet_count = int(toks[0])
            base_count = toks[1] and int(toks[1]) or None
            note_count = tuplet_count
        elif len(toks) == 3:
            tuplet_count = int(toks[0])
            base_count = toks[1] and int(toks[1]) or None
            note_count = toks[2] and int(toks[2]) or tuplet_count
        else:
            raise Exception('Invalid tuplet spec: %s' % val)
        if base_count is None:
            if tuplet_count == 2:
                base_count = 2
            elif tuplet_count in (4, 8):
                base_count = 3
            elif tuplet_count in (3, 6):
                base_count = 2
            # for compound meters, base_count is 3 instead of 2
            elif tuplet_count in (5, 7, 9):
                if (self._meter[0] % 3) == 0:
                    base_count = 3
                else:
                    base_count = 2
        self._tuplet = Tuplet(tuplet_count, base_count)
        self._tuplet_count = note_count
        dispatchList(self, value[3], buf)

    def pitch(self, value, buf):
        self._element = Note(length=self._base_length)
        if self._tuplet_count > 0:
            self._tuplet.add_note(self._element)
            self._tuplet_count -= 1
            if self._tuplet_count == 0:
                self._pending_element = self._tuplet
                self._tuplet = None
        else:
            self._pending_element = self._element
        self._note_augment = 0
        self._note_diminish = 0
        self._note_natural = False
        dispatchList(self, value[3], buf)

    def rest(self, value, buf):
        self._element = Rest(length=self._base_length)
        self._pending_element = self._element
        dispatchList(self, value[3], buf)

    def tie(self, value, buf):
        self._element.tie = True

    def note_length(self, tag, buf):
        val = self.get_string(tag, buf)
        length = self._base_length
        if val in ('/', '//', '///'):
            for i in range(val.count('/')):
                length /= 2
        elif val:
            toks = val.split('/')
            if len(toks) < 2:
                length *= int(val)
            else:
                if toks[0] == '':
                    toks[0] = 2
                if toks[1] == '':
                    toks[1] = 1
                length *= F(int(toks[1]), int(toks[0])) 
        self._element.set_length(length)

    def accidental(self, tag, buf):
        val = self.get_string(tag, buf)
        self._note_augment = val.count('^')
        self._note_diminish = val.count('_')
        self._note_natural = '=' in val

    def octave(self, tag, buf):
        val = self.get_string(tag, buf)
        self._element.change_octave(val.count("'"))
        self._element.change_octave(-val.count(','))

    def ignore(self, value, buf):
        pass

    def traverse(self, value, buf):
        val = []
        if value[3]:
            retval = dispatchList(self, value[3], buf)
            val.extend(retval)
        return val

    def note_element(self, value, buf):
        dispatchList(self, value[3], buf)
        if self._pending_element is not None:
            self.add_element(self._pending_element)
            self._pending_element = None

    space = ignore
    note_stem = traverse
    line_ender = ignore
    gracings = ignore
    begin_slur = ignore
    end_slur = ignore
    grace_notes = ignore
    multi_note = ignore

    ###### helper methods

    def get_string(self, value, buf):
        return buf[value[1]:value[2]]

    def add_element(self, element):
        if self._bar is None or self._bar.is_full():
            self.start_bar()
        self._bar.add_element(element)

    def start_bar(self):
        self._bar = Bar(self._meter)
        self.bars.append(self._bar)

ABC_LINE_EBNF = r'''
root ::= abc_line
abc_line ::= (element+, line_ender)/tex_command/mid_tune_field
>element< ::= nth_repeat/barline/nth_repeat/tuplet_element/begin_slur/end_slur/note_element/space/user_defined
line_ender ::= comment/no_line_break/linefeed/line_break

field_elemskip ::= 'E:', text, end_of_line
field_key ::= "K:", key, end_of_line
field_meter ::= "M:", meter, end_of_line
field_tempo ::= "Q:", tempo, end_of_line
field_title ::= "T:", text, end_of_line

tempo ::= DIGIT+ / ("C", note_length?, "=", DIGIT+)
        / (note_length_strict, "=", DIGIT+)

meter ::= "C|" / "C" / meter_fraction
meter_fraction ::= DIGIT+, "/", DIGIT+

parts ::= part_spec+
part_spec ::= (part / ( "(", part_spec+, ")" ) ), DIGIT*
part ::= [ABCDEFGHIJKLMNOPQRSTUVXYZ]

field_default_length ::= "L:", note_length_strict, end_of_line
note_length_strict ::= DIGIT+, "/", DIGIT+

key ::= key_spec / "HP" / "Hp"
key_spec ::= keynote, mode_spec?, (" ", global_accidental)*
keynote ::= basenote, key_accidental?
key_accidental ::= '#'/'b'
mode_spec ::= " "?, mode, extratext?
mode ::= mode_minor / mode_major / mode_lydian / mode_ionian
        / mode_mixolydian / mode_dorian / mode_aeolian / mode_phrygian
        / mode_locrian
extratext ::= ALPHA*
global_accidental ::= accidental, basenote
mode_minor ::= c'm', c'in'?
mode_major ::= c'maj'
mode_lydian ::= c'lyd'
mode_ionian ::= c'ion'
mode_mixolydian ::= c'mix'
mode_dorian ::= c'dor'
mode_aeolian ::= c'aeo'
mode_phrygian ::= c'phr'
mode_locrian ::= c'loc'

tuplet_element ::= tuplet_spec, note_element+
tuplet_spec ::= '(', DIGIT, (':', DIGIT?, (':', DIGIT?)?)?

note_element ::= note_stem, broken_rhythm?
note_stem ::= guitar_chord?, grace_notes?, gracings*, (note/multi_note)
multi_note ::= '[', note+, ']'
note ::= note_or_rest, note_length?, tie?
>note_or_rest< ::= pitch/rest
pitch ::= accidental?, basenote, octave?
octave ::= ('\''+)/(','+)
note_length ::= '///' / '//' / (DIGIT+, '/', (DIGIT+)?) / (DIGIT+, '/') / ('/', DIGIT+) / '/' / DIGIT+
accidental ::= '^^'/'^'/'__'/'_'/'='
basenote ::= [CDEFGABcdefgab]
rest ::= 'z'
broken_rhythm ::= '<'+/'>'+
tie ::= '-'
gracings ::= [~.vu]
grace_notes ::= '{', pitch+, '}'

guitar_chord ::= '"', (formal_chord/text), '"'
formal_chord ::= basenote, chord_type?, ('/', basenote)?
chord_type ::= 'mmaj7'/'maj7'/'m7'/'mb5'/'m7b5'/'sus4'/'sus'
       /'7sus4'/'dim7'/'dim'/'7b5'/'b6'/'m6'/'mb6'/'46'/'maj9'/'add9'
       /'7b9'/'m9'/'7'/'0'/'o'/'6'/'m'/'9'/'+'

barline ::= '|:' / '|]' / '||' / '|' / '::' / ':|' / '[|'
nth_repeat ::=  ':|2' / '[1' / '[2' / '|1'

begin_slur ::= '('
end_slur ::= ')'

mid_tune_field ::= tune_field
tune_field ::= field_elemskip/field_key/field_default_length
       /field_meter/field_part/field_tempo/field_title
       /field_words
field_part ::= 'P:', part, end_of_line
field_words ::= 'W:', text, end_of_line

user_defined ::= 'H'/'I'/'J'/'K'/'I'/'J'/'K'/'L'/'M'/'N'
	/ 'O'/'P'/'Q'/'R'/'S'/'T'/'U'/'V'/'W'/'X'/'Y'
	/ 'Z'

tex_command ::= '\\', text, linefeed
space ::= [ \t]
comment ::= [%], text, (linefeed/line_break/no_line_break)
line_break ::= '!', linefeed
no_line_break ::= '\\', linefeed
linefeed ::= '\n'
end_of_line ::= (' '/HTAB)*, ('%', text)?, linefeed

<text> ::= text_char*
text_char ::= ALPHA/DIGIT/' '/HTAB/'"'/'!'/'#'/'$'/'&'
       /'\''/'('/')'/'*'/'+'/','/'-'/'.'/'/'/':'/'#'
       /'<'/'='/'>'/'?'/'@'/'['/'\\'/']'/'^'/'_'/'`'
       /'{'/'|'/'}'/'~'

HTAB ::= '\t'
crlf ::= '\r\n'
lf ::= '\n'

<DIGIT> ::= [0-9]
ALPHA ::= [a-zA-Z]
'''

ABCLineParser = Parser(ABC_LINE_EBNF)
